Kotlin之高阶函数与Lambda表达式与闭包

Kotlin之高阶函数与Lambda表达式

在kotlin中,函数和对象一样,都是“一等公民”,这也就表示在kotlin中,函数可以做变量能做的事情,如可以存储在变量与数据结构中、或是作为参数传递给其他高阶函数并且也可以作为高阶函数的返回值、也可以像其他任何非函数值一样调用函数

那么,当函数作为一等公民之后,会给我们的编程带来什么样的变化呢?

当函数作为一等公民之后,我们就能够使用一种新的编程思想——函数式编程。函数式编程是结构化编程的一种,我们在Java中始终使用到的思想是面向对象编程,我们将一切看成是一个一个的对象去处理问题,而函数式编程是将一个一个的函数嵌套起来而得到最后的结果。

高阶函数

高阶函数就是将函数作为参数或返回值的函数

高阶函数的定义应该很好理解,这里我们引用kotlin官方文档的一个例子:

1
2
3
4
5
6
7
fun <T, R> Collection<T>.fold(initial: R, combine: (acc: R, nextElement: T) -> R): R{
var accumulator: R = initial
for(element: T in this){
accumulator = combine(accumulator, element)
}
return accumulator
}

在上面的实例代码中,将一个函数类型 (R, T) -> R作为参数传递给Collection的扩展函数fold。因此我们可以将该fold函数称为高阶函数。

在kotlin中,所有函数类型都可用一个圆括号括起来的参数类型列表与一个返回类型表示,如:(A, B) -> C。表示函数分别接受类型为A和B的两个参数并且返回一个类型为C的返回值。

Lambda表达式

Lambda表达式的意义用一句话来说明就是:Lambda表达式就是一个匿名函数。

Lambda表达式的完整语法可以用如下形式表示:

1
val sum = {x: Int, y: Int -> x + y }

在kotlin中,有一个约定:如果函数的最后一个参数接收函数,那么作为相应的参数传入的Lambda表达式可以放在括号外面(尾随闭包),如下:

1
val product = items.fold(1){acc, e -> acc * e}
  • it:单个参数的隐式名称

一个Lambda表达式只有一个参数是很常见的,如果编译器能够自己识别出参数的类型,那么这个参数的生命可以在调用时忽略, 如:

1
IntArray.filter{ it  > 0}

上述代码的filter时IntArray的一个扩展方法, 上述代码可以获取到一个IntArray中值大于0 的子Array。我们来看一下它的源码中的函数声明:

1
2
3
4
5
6
7
8
9
10
11
/**
* Returns a list containing only elements matching the given [predicate].
*/
public inline fun IntArray.filter(predicate: (Int) -> Boolean): List<Int> {
return filterTo(ArrayList<Int>(), predicate)
}

public inline fun <C : MutableCollection<in Int>> IntArray.filterTo(destination: C, predicate: (Int) -> Boolean): C {
for (element in this) if (predicate(element)) destination.add(element)
return destination
}

可以看到,filterTo方法是通过predicate(element)方法,将符合条件的元素添加到一个新的array并且返回。而filter方法只有一个参数perdicate:(Int) -> Boolean ,因此在之前的调用中,我们可以省略不写并且用 it > 0 表示。

  • lamda表达式的返回值

lamda表达式的返回值如果没有明确的用return说明,则会返回最后一个值。如下:

1
2
3
4
ints.filter{
val shouldFilter = it > 0
shouldFilter //将返回shouldFilter, 等价于 return shouldFilter
}

闭包

闭包是在Javascript中经常用到的一个特性,可以用闭包来完成很多高级特性,也是在函数式编程中经常会用到的一个特性。

对于闭包的概念,先来看两段代码:

1
2
3
4
5
6
7
8
9
10
11
//第一段代码
var count = 2
fun readCount(){
print(count) // count为全局变量, 因此打印出:2
}

//第二段代码
fun countWrapper(){
var count = 2
}
print(count) //error: 因为count为局部变量,作用域为countWrapper内部,因此在外部无法读取

通过上述代码可以看到:如果想要在外部访问某函数内部生命的局部变量,如果直接访问是无法访问的。那么应该如何去访问呢?—— 需要用到闭包。如果使用到闭包的特性,我们可以将上面第二段代码修改称为:

1
2
3
4
5
6
7
8
fun countWrapper(){
var count = 2
return {
print(count)
}
}
var result = countWrapper()
result() //输出:2

可以看到,我们在countWrapper中返回了一个lambda表达式,并打印count。然后我们初始化了该函数的实例result,直接调用result()就能够访问到count的值。那么我个人理解的闭包的概念就是:

闭包:如果一个外部变量由于被Lambda表达式或者匿名内部类函数调用,而导致其生命周期长于原本的作用域,则可以称为闭包。

在kotlin中,lambda表达式或者匿名函数可以访问其闭包(即在外部作用域中的值),并且可以修改闭包中捕获的变量(在Java8中,lambda表达式是无法对其外部作用域中的值作出修改的):

1
2
3
4
5
var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)

可以看出,当使用闭包的时候,存在一个私有作用域,可以访问其外部作用域,可以在将其暴露给外部调用的时候按需修改,并且外部无法直接读取该私有作用域。这样就可以将功能模块拆分为不同的函数,并且函数中的私有作用域存在相互调用的可能,并且相互独立不影响,可以很好的实现函数式编程。